package com.basker.pisces.utils;

import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.nio.charset.Charset;
import java.nio.charset.StandardCharsets;
import java.security.InvalidKeyException;
import java.security.KeyFactory;
import java.security.NoSuchAlgorithmException;
import java.security.PrivateKey;
import java.security.PublicKey;
import java.security.spec.InvalidKeySpecException;
import java.security.spec.PKCS8EncodedKeySpec;
import java.security.spec.X509EncodedKeySpec;
import javax.crypto.BadPaddingException;
import javax.crypto.Cipher;
import javax.crypto.IllegalBlockSizeException;
import javax.crypto.NoSuchPaddingException;
import javax.crypto.SecretKey;
import javax.crypto.SecretKeyFactory;
import javax.crypto.spec.DESedeKeySpec;

import org.apache.commons.codec.digest.DigestUtils;
import org.springframework.lang.Nullable;
import org.springframework.util.Assert;
import org.springframework.util.Base64Utils;

import com.basker.pisces.common.enums.BlockCyperMode;
import com.basker.pisces.constant.SecurityConstants;
import com.basker.pisces.exception.ExceptionFactory;

/**
 * 安全相关的工具类
 *
 * <li>3DES加密与解密；
 * <li>RSA加密与解密
 * <li>MD5和SHA1的处理可以使用{@link DigestUtils}
 *
 * @author hangwen
 */
public class SecurityUtils {

    private static final int RSA_MAX_ENCRYPT_BLOCK = SecurityConstants.RSA_MAX_ENCRYPT_BLOCK;
    private static final int RSA_MAX_DECRYPT_BLOCK = SecurityConstants.RSA_MAX_DECRYPT_BLOCK;

    /**
     * 按指定的块加密模式和密钥，对密文文进行3DES解密
     *
     * @param mode
     * @param key
     * @param encryptBytes
     * @return
     */
    public static byte[] decrypt3Des(BlockCyperMode mode, String key, byte[] encryptBytes) {
        Assert.notNull(mode, "parameter 'mode' is required");
        Assert.hasText(key, "parameter 'key' is required");
        Assert.notNull(encryptBytes, "parameter 'encryptBytes' is required");

        try {
            SecretKey secretKey = generateThreeDesKey(key);
            Cipher cipher = getCipher(SecurityConstants.THREE_DES_ALGORITHM, mode.asString(),
                    SecurityConstants.PADDING_MODE_PKCS5);

            cipher.init(Cipher.DECRYPT_MODE, secretKey);

            return cipher.doFinal(encryptBytes);
        } catch (InvalidKeyException | NoSuchAlgorithmException | InvalidKeySpecException | NoSuchPaddingException
                | IllegalBlockSizeException | BadPaddingException e) {
            throw ExceptionFactory.createRuntimeException(e);
        }
    }

    /**
     * 按指定的块加密模式和密钥，对密文文进行3DES解密,然后按UTF-8对解密后的数据进行解码
     *
     * @param mode
     * @param key
     * @param encryptBytes
     * @return
     */
    public static String decrypt3DesAsString(BlockCyperMode mode, String key, byte[] encryptBytes) {
        return decrypt3DesAsString(mode, key, encryptBytes, StandardCharsets.UTF_8);
    }

    /**
     * 按指定的块加密模式和密钥，对密文文进行3DES解密,然后按指定字符集对解密后的数据进行解码
     *
     * @param mode
     * @param key
     * @param encryptBytes
     * @param charset
     * @return
     */
    public static String decrypt3DesAsString(BlockCyperMode mode, String key, byte[] encryptBytes,
                                             @Nullable Charset charset) {
        byte[] decryptBytes = decrypt3Des(mode, key, encryptBytes);

        charset = charset != null ? charset : StandardCharsets.UTF_8;
        return new String(decryptBytes, charset);
    }

    /**
     * 按指定的块加密模式和密钥,对Base64编码的密文进行3DES解密后，按UTF-8字符集解码
     *
     * @param mode
     * @param key
     * @param encryptText
     * @return
     */
    public static String decrypt3DesFromBase64(BlockCyperMode mode, String key, String encryptText) {
        byte[] encryptBytes = Base64Utils.decodeFromString(encryptText);
        return decrypt3DesAsString(mode, key, encryptBytes);
    }

    /**
     * 按指定的块加密模式和密钥,对Base64编码的密文进行3DES解密后，按指定字符集解码
     *
     * @param mode
     * @param key
     * @param encryptText
     * @param charset
     * @return
     */
    public static String decrypt3DesFromBase64(BlockCyperMode mode, String key, String encryptText,
                                               @Nullable Charset charset) {
        byte[] encryptBytes = Base64Utils.decodeFromString(encryptText);
        return decrypt3DesAsString(mode, key, encryptBytes, charset);
    }

    /**
     * 按指定的块加密模式和私有钥，对密文文进行RSA解密
     *
     * @param mode
     * @param key
     * @param encryptBytes
     * @return
     */
    public static byte[] decryptRSA(BlockCyperMode mode, String key, byte[] encryptBytes) {
        Assert.notNull(mode, "parameter 'mode' is required");
        Assert.hasText(key, "parameter 'key' is required");
        Assert.notNull(encryptBytes, "parameter 'encryptBytes' is required");

        try {
            PrivateKey privateKey = generatePrivateKey(key);
            Cipher cipher = getCipher(SecurityConstants.RSA_ALGORITHM, mode.asString(),
                    SecurityConstants.PADDING_MODE_PKCS1);

            cipher.init(Cipher.DECRYPT_MODE, privateKey);

            // 分段解密
            // 1024位的证书，解密时最大支持为128,2048位的证书，解密时最大支持为256,这里取128
            try (ByteArrayOutputStream out = new ByteArrayOutputStream()) {
                int length = encryptBytes.length;
                int offSet = 0;

                for (int i = 0; length - offSet > 0; offSet = i * RSA_MAX_DECRYPT_BLOCK) {
                    byte[] cache;
                    if (length - offSet > RSA_MAX_DECRYPT_BLOCK) {
                        cache = cipher.doFinal(encryptBytes, offSet, RSA_MAX_DECRYPT_BLOCK);
                    } else {
                        cache = cipher.doFinal(encryptBytes, offSet, length - offSet);
                    }

                    out.write(cache, 0, cache.length);
                    ++i;
                }

                return out.toByteArray();
            }
        } catch (InvalidKeyException | NoSuchAlgorithmException | InvalidKeySpecException | NoSuchPaddingException
                | IllegalBlockSizeException | BadPaddingException | IOException e) {
            throw ExceptionFactory.createRuntimeException(e);
        }
    }

    /**
     * 按指定的块加密模式和私钥，对密文文进行RSA解密,然后按UTF-8对解密后的数据进行解码
     *
     * @param mode
     * @param key
     * @param encryptBytes
     * @return
     */
    public static String decryptRSAAsString(BlockCyperMode mode, String key, byte[] encryptBytes) {
        return decryptRSAAsString(mode, key, encryptBytes, StandardCharsets.UTF_8);
    }

    /**
     * 按指定的块加密模式和私钥，对密文文进行RSA解密,然后按指定字符集对解密后的数据进行解码
     *
     * @param mode
     * @param key
     * @param encryptBytes
     * @param charset
     * @return
     */
    public static String decryptRSAAsString(BlockCyperMode mode, String key, byte[] encryptBytes,
                                            @Nullable Charset charset) {
        byte[] decryptBytes = decryptRSA(mode, key, encryptBytes);

        charset = charset != null ? charset : StandardCharsets.UTF_8;
        return new String(decryptBytes, charset);
    }

    /**
     * 按指定的块加密模式和私钥,对Base64编码的密文进行RSA解密后，按UTF-8字符集解码
     *
     * @param mode
     * @param key
     * @param encryptText
     * @return
     */
    public static String decryptRSAFromBase64(BlockCyperMode mode, String key, String encryptText) {
        byte[] encryptBytes = Base64Utils.decodeFromString(encryptText);
        return decryptRSAAsString(mode, key, encryptBytes);
    }

    /**
     * 按指定的块加密模式和私钥,对Base64编码的密文进行RSA解密后，按指定字符集解码
     *
     * @param mode
     * @param key
     * @param encryptText
     * @param charset
     * @return
     */
    public static String decryptRSAFromBase64(BlockCyperMode mode, String key, String encryptText,
                                              @Nullable Charset charset) {
        byte[] encryptBytes = Base64Utils.decodeFromString(encryptText);
        return decryptRSAAsString(mode, key, encryptBytes, charset);
    }

    /**
     * 按指定的块加密模式和密钥，对原始字节数据进行3DES加密
     *
     * @param mode
     * @param key
     * @param original
     * @return
     */
    public static byte[] encrypt3Des(BlockCyperMode mode, String key, byte[] original) {
        Assert.notNull(mode, "parameter 'mode' is required");
        Assert.hasText(key, "parameter 'key' is required");
        Assert.notNull(original, "parameter 'original' is required");

        try {
            SecretKey secretKey = generateThreeDesKey(key);

            Cipher cipher = getCipher(SecurityConstants.THREE_DES_ALGORITHM, mode.asString(),
                    SecurityConstants.PADDING_MODE_PKCS5);
            cipher.init(Cipher.ENCRYPT_MODE, secretKey);

            return cipher.doFinal(original);
        } catch (InvalidKeyException | NoSuchAlgorithmException | InvalidKeySpecException | NoSuchPaddingException
                | IllegalBlockSizeException | BadPaddingException e) {
            throw ExceptionFactory.createRuntimeException(e);
        }
    }

    /**
     * 按指定的块加密模式和密钥，对明文按UTF-8编码后进行3DES加密
     *
     * @param mode
     * @param key
     * @param plaintext
     * @return
     */
    public static byte[] encrypt3Des(BlockCyperMode mode, String key, String plaintext) {
        return encrypt3Des(mode, key, plaintext, StandardCharsets.UTF_8);
    }

    /**
     * 按指定的块加密模式和密钥，对明文按指定字符集编码后进行3DES加密
     *
     * @param mode
     * @param key
     * @param plaintext
     * @param charset
     * @return
     */
    public static byte[] encrypt3Des(BlockCyperMode mode, String key, String plaintext, @Nullable Charset charset) {
        Assert.hasText(plaintext, "parameter 'plaintext' is required");
        charset = charset != null ? charset : StandardCharsets.UTF_8;

        return encrypt3Des(mode, key, plaintext.getBytes(charset));
    }

    /**
     * 按指定的块加密模式和密钥，对明文按UTF-8编码后，进行3DES加密,密文按Base64编码的字符串返回
     *
     * @param mode
     * @param key
     * @param plaintext
     * @return
     */
    public static String encrypt3DesAsBase64(BlockCyperMode mode, String key, String plaintext) {
        byte[] encryptBytes = encrypt3Des(mode, key, plaintext);
        return Base64Utils.encodeToString(encryptBytes);
    }

    /**
     * 按指定的块加密模式和密钥，对明文按指定字符集编码后，进行3DES加密,密文按Base64编码的字符串返回
     *
     * @param mode
     * @param key
     * @param plaintext
     * @param charset
     * @return
     */
    public static String encrypt3DesAsBase64(BlockCyperMode mode, String key, String plaintext, Charset charset) {
        byte[] encryptBytes = encrypt3Des(mode, key, plaintext, charset);
        return Base64Utils.encodeToString(encryptBytes);
    }

    /**
     * 按指定的块加密模式和公钥,对原始字节数据进行RSA加密
     *
     * @param mode
     * @param key
     * @param original
     * @return
     */
    public static byte[] encryptRSA(BlockCyperMode mode, String key, byte[] original) {
        Assert.notNull(mode, "parameter 'mode' is required");
        Assert.hasText(key, "parameter 'key' is required");
        Assert.notNull(original, "parameter 'original' is required");

        try {
            PublicKey publicKey = generateRSAPublicKey(key);

            Cipher cipher = getCipher(SecurityConstants.RSA_ALGORITHM, mode.asString(),
                    SecurityConstants.PADDING_MODE_PKCS1);
            cipher.init(Cipher.ENCRYPT_MODE, publicKey);

            // 分段加密
            // 1024位的证书，加密时最大支持117个字节,2048位的证书，加密时最大支持245个字节,这里117
            try (ByteArrayOutputStream out = new ByteArrayOutputStream()) {
                int length = original.length;
                int offSet = 0;

                for (int i = 0; length - offSet > 0; offSet = i * RSA_MAX_ENCRYPT_BLOCK) {
                    byte[] cache;
                    if (length - offSet > RSA_MAX_ENCRYPT_BLOCK) {
                        cache = cipher.doFinal(original, offSet, RSA_MAX_ENCRYPT_BLOCK);
                    } else {
                        cache = cipher.doFinal(original, offSet, length - offSet);
                    }

                    out.write(cache, 0, cache.length);
                    ++i;
                }

                return out.toByteArray();
            }
        } catch (NoSuchAlgorithmException | InvalidKeySpecException | NoSuchPaddingException | InvalidKeyException
                | IllegalBlockSizeException | BadPaddingException | IOException e) {
            throw ExceptionFactory.createRuntimeException(e);
        }
    }

    /**
     * 按指定的块加密模式和公钥，对明文按指定字符集编码后进行RSA加密
     *
     * @param mode
     * @param key
     * @param original
     * @return
     */
    public static byte[] encryptRSA(BlockCyperMode mode, String key, String plaintext) {
        return encryptRSA(mode, key, plaintext, StandardCharsets.UTF_8);
    }

    /**
     * 按指定的块加密模式和公钥，使用指定公钥，对明文按指定字符集编码后进行RSA加密
     *
     * @param mode
     * @param key
     * @param plaintext
     * @param charset
     * @return
     */
    public static byte[] encryptRSA(BlockCyperMode mode, String key, String plaintext, @Nullable Charset charset) {
        Assert.hasText(plaintext, "parameter 'plaintext' is required");
        charset = charset != null ? charset : StandardCharsets.UTF_8;

        return encryptRSA(mode, key, plaintext.getBytes(charset));
    }

    /**
     * 按指定的块加密模式和公钥，对明文按utf-8字符集编码后，进行RSA加密,密文按Base64编码的字符串返回
     *
     * @param mode
     * @param key
     * @param plaintext
     * @return
     */
    public static String encryptRSAAsBase64(BlockCyperMode mode, String key, String plaintext) {
        byte[] encryptBytes = encryptRSA(mode, key, plaintext);
        return Base64Utils.encodeToString(encryptBytes);
    }

    /**
     * 按指定的块加密模式和公钥，对明文按指定字符集编码后，进行RSA加密,密文按Base64编码的字符串返回
     *
     * @param mode
     * @param key
     * @param plaintext
     * @param charset
     * @return
     */
    public static String encryptRSAAsBase64(BlockCyperMode mode, String key, String plaintext, Charset charset) {
        byte[] encryptBytes = encryptRSA(mode, key, plaintext, charset);
        return Base64Utils.encodeToString(encryptBytes);
    }

    private static PrivateKey generatePrivateKey(String key) throws NoSuchAlgorithmException, InvalidKeySpecException {
        byte[] decodedKey = Base64Utils.decodeFromString(key);
        PKCS8EncodedKeySpec keySpec = new PKCS8EncodedKeySpec(decodedKey);
        KeyFactory keyFactory = KeyFactory.getInstance(SecurityConstants.RSA_ALGORITHM);

        return keyFactory.generatePrivate(keySpec);
    }

    private static PublicKey generateRSAPublicKey(String key) throws NoSuchAlgorithmException, InvalidKeySpecException {
        byte[] decodedKey = Base64Utils.decodeFromString(key);
        X509EncodedKeySpec keySpec = new X509EncodedKeySpec(decodedKey);
        KeyFactory keyFactory = KeyFactory.getInstance(SecurityConstants.RSA_ALGORITHM);

        return keyFactory.generatePublic(keySpec);
    }

    private static SecretKey generateThreeDesKey(String key)
            throws InvalidKeyException, NoSuchAlgorithmException, InvalidKeySpecException {
        DESedeKeySpec keySpec = new DESedeKeySpec(key.getBytes(StandardCharsets.UTF_8));
        SecretKeyFactory keyFactory = SecretKeyFactory.getInstance(SecurityConstants.THREE_DES_ALGORITHM);

        return keyFactory.generateSecret(keySpec);
    }

    private static Cipher getCipher(String algorithm, String cyperMode, String fillMode)
            throws NoSuchAlgorithmException, NoSuchPaddingException {
        return Cipher.getInstance(String.join("/", algorithm, cyperMode, fillMode));
    }

}
